package com.softwaremill.common.dbtest; import bitronix.tm.BitronixTransactionManager; import bitronix.tm.TransactionManagerServices; import bitronix.tm.resource.jdbc.PoolingDataSource; import org.apache.log4j.BasicConfigurator; import org.apache.log4j.Level; import org.h2.jdbcx.JdbcDataSource; import org.hibernate.cfg.Environment; import org.hibernate.dialect.H2Dialect; import org.hibernate.ejb.AvailableSettings; import org.hibernate.ejb.Ejb3Configuration; import org.hibernate.transaction.BTMTransactionManagerLookup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterSuite; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeSuite; import com.softwaremill.common.arquillian.BetterArquillian; import com.softwaremill.common.cdi.persistence.EntityManagerFactoryProducer; import com.softwaremill.common.dbtest.util.DbMode; import com.softwaremill.common.util.dependency.BeanManagerDependencyProvider; import com.softwaremill.common.util.dependency.D; import javax.enterprise.inject.spi.BeanManager; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.transaction.Status; import java.util.Properties; /** * Allows to create fully integration test with real transactions * Take a look on TestTransactionalDBTest */ public abstract class TransactionalDBTest extends BetterArquillian { private static final Logger LOG = LoggerFactory.getLogger(TransactionalDBTest.class); /** * Used to configure Transaction Manager * * - bitronix.uniqueName - name to be used with JNDI to identify given Transaction Manager * - bitronix.file - full path to the Bitronix Transaction Manager configuration file * follow the link for more details * http://docs.codehaus.org/display/BTM/Configuration2x */ protected static final String BITRONIX_UNIQUE_NAME = "bitronix.uniqueName"; protected static final String BITRONIX_FILE = "bitronix.file"; private static final String BITRONIX_DATASOURCE_CLASS = "bitronix.datasource.class"; private static final String BITRONIX_CONNECTION_USERNAME = "bitronix.connection.username"; private static final String BITRONIX_CONNECTION_URL = "bitronix.connection.url"; protected EntityManagerFactory emf; @Inject private BeanManager bm; private BeanManagerDependencyProvider depProvider; private UtxDependencyProvider utxProvider; @BeforeSuite public void beforeSuite() throws Exception { setupLog4J(); callBeforeSuite(); } @AfterSuite public void afterSuite() throws Exception { callAfterSuite(); } @BeforeClass public void beforeClass() throws Exception { callBeforeClass(); Ejb3Configuration cfg = new Ejb3Configuration(); configureTransactionManager(cfg); initTransactionManager(cfg); configureDatabase(cfg); configureEntities(cfg); emf = cfg.buildEntityManagerFactory(); // Setting the EMF so that it's produced correctly EntityManagerFactoryProducer.setStaticEntityManagerFactory(emf); initTestData(); } protected void initTransactionManager(Ejb3Configuration cfg) throws Exception{ String configurationFile = getConfigurationFile(); if (configurationFile != null) { LOG.info("Using [{}] as a configuration file for Bitronix Transaction Manager!", configurationFile); TransactionManagerServices.getConfiguration().setResourceConfigurationFilename(configurationFile); } else { // create InMemory H2 database PoolingDataSource xa = new PoolingDataSource(); xa.setUniqueName(cfg.getProperties().getProperty(BITRONIX_UNIQUE_NAME, "test-" + getClass().getSimpleName())); xa.setClassName(cfg.getProperties().getProperty(BITRONIX_DATASOURCE_CLASS, JdbcDataSource.class.getName())); xa.setAllowLocalTransactions(true); xa.setMaxPoolSize(3); xa.setMinPoolSize(1); Properties prop = new Properties(); String url = cfg.getProperties().getProperty(BITRONIX_CONNECTION_URL, "jdbc:h2:mem:" + getClass().getSimpleName()); if (compatibilityMode() != null) { url += ";MODE=" + compatibilityMode(); } prop.setProperty("URL", url); prop.setProperty("user", cfg.getProperties().getProperty(BITRONIX_CONNECTION_USERNAME, "sa")); xa.setDriverProperties(prop); xa.init(); TransactionManagerServices.getResourceLoader().getResources().put(xa.getUniqueName(), xa); } TransactionManagerServices.getResourceLoader().init(); } protected String getConfigurationFile() throws Exception { return null; } protected void initTestData() throws Exception { EntityManager em = emf.createEntityManager(); loadTestData(em); } @AfterClass public void afterClass() throws Exception { callAfterClass(); shutdownEmf(); shutdownTransactionManager(); D.unregister(depProvider); D.unregister(utxProvider); } @BeforeMethod public void beforeMethod() throws Exception { callBeforeMethod(); registerBeanManager(); registerUtxProvider(); } protected void registerUtxProvider() { if (utxProvider == null) { utxProvider = new UtxDependencyProvider(); D.register(utxProvider); } } protected void registerBeanManager() { if (depProvider == null) { depProvider = new BeanManagerDependencyProvider(bm); D.register(depProvider); } } @AfterMethod public void afterMethod() throws Exception { callAfterMethod(); } /** * Override to setup test on BeforeSuite */ protected void callBeforeSuite() throws Exception { } /** * Override to tear down test on AfterSuite */ protected void callAfterSuite() throws Exception { } /** * Override to setup test on BeforeClass */ protected void callBeforeClass() throws Exception { } /** * Override to tear down test on AfterClass */ protected void callAfterClass() throws Exception { } /** * Override to setup test on BeforeMethod */ protected void callBeforeMethod() throws Exception { } /** * Override to tear down test on AfterMethod */ protected void callAfterMethod() throws Exception { } /** * Override to setup Log4j */ protected void setupLog4J() { BasicConfigurator.configure(); org.apache.log4j.Logger.getRootLogger().setLevel(Level.INFO); } /** * Used to setup TransactionManager * * @param cfg current H3 configuration */ protected void configureTransactionManager(Ejb3Configuration cfg) { cfg.setProperty(Environment.TRANSACTION_MANAGER_STRATEGY, BTMTransactionManagerLookup.class.getName()); cfg.setProperty(Environment.CURRENT_SESSION_CONTEXT_CLASS, "jta"); cfg.setProperty(AvailableSettings.TRANSACTION_TYPE, "JTA"); } protected void configureDatabase(Ejb3Configuration cfg) { cfg.setProperty(Environment.HBM2DDL_AUTO, "create"); cfg.setProperty(Environment.RELEASE_CONNECTIONS, "after_statement"); cfg.setProperty(Environment.DATASOURCE, "test-" + getClass().getSimpleName()); cfg.setProperty(Environment.DIALECT, H2Dialect.class.getName()); } protected abstract void configureEntities(Ejb3Configuration cfg); protected void shutdownTransactionManager() { TransactionManagerServices.getTransactionManager().shutdown(); } protected void shutdownEmf() { emf.close(); } protected void beginTransaction() throws Exception { BitronixTransactionManager tm = TransactionManagerServices.getTransactionManager(); if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { throw new RuntimeException("Active transaction in progress! Please commit or roll it back!"); } tm.begin(); } protected void commitTransaction() throws Exception { BitronixTransactionManager tm = TransactionManagerServices.getTransactionManager(); if (tm.getStatus() != Status.STATUS_ACTIVE) { throw new RuntimeException("Transaction is not active and cannot be committed! Please start a new transaction first!"); } tm.commit(); } protected void rollbackTransaction() throws Exception { BitronixTransactionManager tm = TransactionManagerServices.getTransactionManager(); if (tm.getStatus() != Status.STATUS_ACTIVE && tm.getStatus() != Status.STATUS_MARKED_ROLLBACK) { throw new RuntimeException("Transaction is not active and cannot be rolled back! Please start a new transaction first!"); } tm.rollback(); } /** * Override to provide support for H2 compatibility mode * * @return DbMode to use with H2, null means don't use compatibility mode and act as a standard H2 */ protected DbMode compatibilityMode() { return null; } /** * Override to load test data into database used for test, the steps are as follow: * - beginTransaction() * - em.joinTransaction() * - ... test data ... * - commitTransaction() / rollbackTransaction() * - em.close() * * EM is working with real transactions so you must either commit or rollback active transaction. * * @param em active EM * @throws Exception */ protected abstract void loadTestData(EntityManager em) throws Exception; }